package jmine.tec.utils.reflection;

import java.io.Serializable;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;

import jmine.tec.annotations.Unmodifiable;

/**
 * Implementacao de {@link GenericTypeResolver}
 */
@Unmodifiable
public final class GenericTypeResolverImpl implements GenericTypeResolver, Serializable {

    private static final GenericTypeResolver INSTANCE = new GenericTypeResolverImpl();

    /**
     * @return {@link GenericTypeResolver}
     */
    public static GenericTypeResolver getInstance() {
        return INSTANCE;
    }

    /**
     * @param anyType the type to resolve
     * @param containingClass the containg class
     * @param typeIndex the index of the formal type to fetch
     * @return Class
     */
    public Class<?> resolveTypeForParameterizedType(ParameterizedType anyType, Class<?> containingClass, int typeIndex) {
        Class<?> type = this.resolveTypeOrNull(anyType, containingClass, typeIndex);
        if (type == null) {
            throw new IllegalArgumentException("no formal type parameter found");
        }
        return type;
    }

    /**
     * @param anyType the type to resolve
     * @param typeIndex the index of the formal type to fetch
     * @return Class
     */
    public Class<?> resolveTypeForFixedParameterizedType(ParameterizedType anyType, int typeIndex) {
        Type type = anyType.getActualTypeArguments()[typeIndex];
        if (type instanceof Class<?>) {
            return (Class<?>) type;
        }
        return null;
    }

    /**
     * Resolve o {@link ParameterizedType} dado ou devolve null se nao for possivel
     * 
     * @param anyType o tipo
     * @param containingClass a classe que declara o tipo generico passado
     * @param typeIndex o indice
     * @return {@link Class}
     */
    public Class<?> resolveTypeOrNull(ParameterizedType anyType, Class<?> containingClass, int typeIndex) {
        if (anyType == null || containingClass == null) {
            throw new NullPointerException();
        }
        Type[] arguments = anyType.getActualTypeArguments();
        if (arguments == null || arguments.length == 0) {
            throw new IllegalArgumentException("no type parameters on parameterized type");
        }
        if (arguments.length < typeIndex) {
            throw new IllegalArgumentException("index out of bounds - no formal type parameter with index: " + typeIndex);
        }
        Type desired = arguments[typeIndex];
        return this.resolveAnyTypeOrNull(desired, containingClass);
    }

    /**
     * Resolve um wildcard type, devolvendo o n-esimo upper bound, dado pelo parametro wildCardIndex. Resolve o upper bound se este for um
     * {@link TypeVariable}. Se o upper bound for um ParameterizedType, tenta resolver seu 'rawType'.
     * 
     * @param wild the {@link WildcardType}
     * @param containingClass the containing class
     * @param wildCardIndex o indice do 'bound' do wild card
     * @return class
     */
    public Class<?> resolveWildcardTypeOrNull(WildcardType wild, Class<?> containingClass, int wildCardIndex) {
        final Type bound = wild.getUpperBounds()[wildCardIndex];
        return resolveAnyTypeOrNull(bound, containingClass);
    }

    /**
     * @param type representacao do tipo generico (formal)
     * @param containingClass a classe que define o tipo generico, se houver
     * @return Class
     */
    public Class<?> resolveTypeForFixedGenericType(TypeVariable<?> type, Class<?> containingClass) {
        Class<?> ot = this.resolveTypeVariableOrNull(type, containingClass);
        if (ot == null) {
            throw new IllegalArgumentException("no formal type parameter found");
        }
        return ot;
    }

    /**
     * Resolve o {@link TypeVariable} ou devolve null
     * 
     * @param type o tipo
     * @param containingClass a classe que declara
     * @return {@link Class} ou <code>null</code>
     */
    public Class<?> resolveTypeVariableOrNull(TypeVariable<?> type, Class<?> containingClass) {
        if (type == null || containingClass == null) {
            throw new NullPointerException();
        }
        ParameterizedType paramType = this.findGenericTypeDeclaring(type, containingClass);
        if (paramType == null) {
            Type[] bounds = type.getBounds();
            for (int i = 0; i < bounds.length; i++) {
                Class<?> resolved = resolveAnyTypeOrNull(bounds[i], containingClass);
                if (resolved != null) {
                    return resolved;
                }
            }
            return null;
        }
        final Type paramTypeOwner = paramType.getRawType();
        // resolver este tipo primeiro
        Class<?> ownerType = this.resolveAnyTypeOrNull(paramTypeOwner, containingClass);
        Type desiredType = paramType.getActualTypeArguments()[this.findIndexForVariable(type, ownerType)];
        if (desiredType instanceof Class<?>) {
            return (Class<?>) desiredType;
        } else {
            return this.resolveAnyTypeOrNull(desiredType, containingClass);
        }
    }

    /**
     * Resolve um tipo qualquer
     * 
     * @param type o tipo a ser resolvido
     * @param containing a classe cujo tipo está fixo
     * @return Class
     */
    public Class<?> resolveAnyTypeOrNull(Type type, Class<?> containing) {
        if (type instanceof Class<?>) {
            return (Class<?>) type;
        } else if (type instanceof TypeVariable<?>) {
            return this.resolveTypeVariableOrNull((TypeVariable<?>) type, containing);
        } else if (type instanceof WildcardType) {
            WildcardType wildCartType = (WildcardType) type;
            for (int i = 0; i < wildCartType.getUpperBounds().length; i++) {
                Class<?> resolved = resolveWildcardTypeOrNull(wildCartType, containing, i);
                if (resolved != null) {
                    return resolved;
                }
            }
            return null;
        } else if (type instanceof ParameterizedType && ((ParameterizedType) type).getRawType() instanceof Class<?>) {
            Type rawType = ((ParameterizedType) type).getRawType();
            return (Class<?>) rawType;
        }
        return null;
    }

    /**
     * Devolve o indice que o {@link TypeVariable} passado declarado na {@link Class} passada representa.
     * 
     * @param variable o {@link TypeVariable}
     * @param containing a classe que declara o {@link TypeVariable}
     * @return int indice
     */
    public int findIndexForVariable(TypeVariable<?> variable, Class<?> containing) {
        TypeVariable<?>[] typeParameters = containing.getTypeParameters();
        for (int i = 0; i < typeParameters.length; i++) {
            if (typeParameters[i].equals(variable)) {
                return i;
            }
        }
        throw new IllegalArgumentException("type variable " + variable.getName() + " not found on type: " + containing);
    }

    /**
     * Procura na hierarquia de toCheck, a classe generica que declara o tipo passado.
     * 
     * @param declaredType {@link TypeVariable}
     * @param toCheck {@link Class}
     * @return {@link ParameterizedType}
     */
    private ParameterizedType findGenericTypeDeclaring(TypeVariable<?> declaredType, Class<?> toCheck) {
        if (toCheck == null || toCheck == Object.class) {
            return null;
        }
        Type genericSuperclass = toCheck.getGenericSuperclass();
        Object genericDeclaration = declaredType.getGenericDeclaration();
        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType paramType = (ParameterizedType) genericSuperclass;
            if (genericDeclaration == paramType.getRawType()) {
                return paramType;
            }
        }
        Type[] genericInterfaces = toCheck.getGenericInterfaces();
        for (Type type : genericInterfaces) {
            if (type instanceof ParameterizedType) {
                ParameterizedType paramType = (ParameterizedType) type;
                if (genericDeclaration == paramType.getRawType()) {
                    return paramType;
                }
            }
        }
        return this.findGenericTypeDeclaring(declaredType, toCheck.getSuperclass());
    }

    /**
     * @param type the type
     * @param containingClass the containing class
     * @return Class
     */
    public Class<?> resolveComponentTypeForFixedGenericArrayType(GenericArrayType type, Class<?> containingClass) {
        Class<?> resolved = this.resolveArrayTypeOrNull(type, containingClass);
        if (resolved == null) {
            throw new IllegalArgumentException("no formal type parameter found");
        }
        return resolved;
    }

    /**
     * Resolve o tipo do array ou null
     * 
     * @param type o tipo
     * @param containingClass a classe que declara
     * @return {@link Class} ou <code>null</code>
     */
    public Class<?> resolveArrayTypeOrNull(GenericArrayType type, Class<?> containingClass) {
        Type componentType = type.getGenericComponentType();
        if (componentType instanceof Class<?>) {
            return (Class<?>) componentType;
        }
        if (componentType instanceof TypeVariable<?>) {
            return this.resolveTypeForFixedGenericType((TypeVariable<?>) componentType, containingClass);
        }
        if (componentType instanceof WildcardType) {
            return this.resolveWildcardTypeOrNull((WildcardType) componentType, containingClass, 0);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isResolvable(Type genericType, Class<?> declaringClass) {
        return this.resolveAnyTypeOrNull(genericType, declaringClass) != null;
    }

}
